/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.bizcreator.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Class which executes {@link Command}s and {@link IncrementalCommand}s after
 * all currently pending event handlers have completed. This class attempts to
 * protect against slow script warnings by running commands in small time
 * increments.
 *
 * <p>
 * It is still possible that a poorly written command could cause a slow script
 * warning which a user may choose to cancel. In that event, a
 * {@link CommandCanceledException} or an
 * {@link IncrementalCommandCanceledException} is reported through the current
 * {@link UncaughtExceptionHandler} depending on the type of command which
 * caused the warning. All other commands will continue to be executed.
 * </p>
 *
 * TODO(mmendez): Can an SSW be detected without using a timer? Currently, if a
 * {@link Command} or an {@link IncrementalCommand} calls either
 * {@link Window#alert(String)} or the JavaScript <code>alert(String)</code>
 * methods directly or indirectly then the  cancellation timer can fire,
 * resulting in a false SSW cancellation detection.
 */
class CommandExecutor {

    /**
     * A circular iterator used by this class. This iterator will wrap back to
     * zero when it hits the end of the commands.
     */
    private class CircularIterator implements Iterator<Object> {

        /**
         * Index of the element where this iterator should wrap back to the
         * beginning of the collection.
         */
        private int end;
        /**
         * Index of the last item returned by {@link #next()}.
         */
        private int last = -1;
        /**
         * Index of the next command to execute.
         */
        private int next = 0;

        /**
         * Returns <code>true</code> if there are more commands in the queue.
         *
         * @return <code>true</code> if there are more commands in the queue.
         */
        public boolean hasNext() {
            return next < end;
        }

        /**
         * Returns the next command from the queue. When the end of the dispatch
         * region is reached it will wrap back to the start.
         *
         * @return next command from the queue.
         */
        public Object next() {
            last = next;
            Object command = commands.get(next++);
            if (next >= end) {
                next = 0;
            }

            return command;
        }

        /**
         * Removes the command which was previously returned by {@link #next()}.
         *
         */
        public void remove() {
            assert (last >= 0);

            commands.remove(last);
            --end;

            if (last <= next) {
                if (--next < 0) {
                    next = 0;
                }
            }

            last = -1;
        }

        /**
         * Returns the last element returned by {@link #next()}.
         *
         * @return last element returned by {@link #next()}
         */
        private Object getLast() {
            assert (last >= 0);
            return commands.get(last);
        }

        private void setEnd(int end) {
            assert (end >= next);

            this.end = end;
        }

        private void setLast(int last) {
            this.last = last;
        }

        private boolean wasRemoved() {
            return last == -1;
        }
    }
    /**
     * Default amount of time to wait before assuming that a script cancellation
     * has taken place. This should be a platform dependent value, ultimately we
     * may need to acquire this value based on a rebind decision. For now, we
     * chose the smallest value known to cause an SSW.
     */
    private static final int DEFAULT_CANCELLATION_TIMEOUT_MILLIS = 10000;
    /**
     * Default amount of time to spend dispatching commands before we yield to the
     * system.
     */
    private static final int DEFAULT_TIME_SLICE_MILLIS = 100;

    /**
     * Returns true the end time has been reached or exceeded.
     *
     * @param currentTimeMillis current time in milliseconds
     * @param startTimeMillis end time in milliseconds
     * @return true if the end time has been reached
     */
    private static boolean hasTimeSliceExpired(double currentTimeMillis,
            double startTimeMillis) {
        return currentTimeMillis - startTimeMillis >= DEFAULT_TIME_SLICE_MILLIS;
    }
    /**
     * Timer used to recover from script cancellations arising from slow script
     * warnings.
     */
    private final Timer timer = new Timer();
    /*
    private final Timer cancellationTimer = new Timer() {
    @Override
    public void run() {
    if (!isExecuting()) {

    //If we are not executing, then the cancellation timer expired right
    //about the time that the command dispatcher finished -- we are okay so
    //we just exit.

    return;
    }

    doCommandCanceled();
    }
    };*/
    private final TimerTask cancellationTask = new TimerTask() {

        @Override
        public void run() {
            if (!isExecuting()) {

                //If we are not executing, then the cancellation timer expired right
                //about the time that the command dispatcher finished -- we are okay so
                //we just exit.

                return;
            }

            doCommandCanceled();
        }
    };
    /**
     * Commands that need to be executed.
     */
    private final List<Object> commands = new ArrayList<Object>();
    /**
     * Set to <code>true</code> when we are actively dispatching commands.
     */
    private boolean executing = false;
    /**
     * Timer used to drive the dispatching of commands in the background.
     */
    /*
    private final Timer executionTimer = new Timer() {
        @Override
        public void run() {
            assert (!isExecuting());

            setExecutionTimerPending(false);

            doExecuteCommands(Duration.currentTimeMillis());
        }
    };*/
    private final TimerTask executionTask = new TimerTask(){

        @Override
        public void run() {
            assert (!isExecuting());

            setExecutionTimerPending(false);

            doExecuteCommands(System.currentTimeMillis());
        }
    };
    
    /**
     * Set to <code>true</code> when we are waiting for a dispatch timer event
     * to fire.
     */
    private boolean executionTimerPending = false;
    /**
     * The single circular iterator instance that we use to iterate over the
     * collection of commands.
     */
    private final CircularIterator iterator = new CircularIterator();

    /**
     * Submits a {@link Command} for execution.
     *
     * @param command command to submit
     */
    public void submit(Command command) {
        commands.add(command);

        maybeStartExecutionTimer();
    }

    /**
     * Submits an {@link IncrementalCommand} for execution.
     *
     * @param command command to submit
     */
    public void submit(IncrementalCommand command) {
        commands.add(command);

        maybeStartExecutionTimer();
    }

    /**
     * Reports either a {@link CommandCanceledException} or an
     * {@link IncrementalCommandCanceledException} back through the
     * {@link UncaughtExceptionHandler} if one is set.
     */
    protected void doCommandCanceled() {
        Object cmd = iterator.getLast();
        iterator.remove();
        assert (cmd != null);

        RuntimeException ex = null;
        if (cmd instanceof Command) {
            ex = new CommandCanceledException((Command) cmd);
        } else if (cmd instanceof IncrementalCommand) {
            ex = new IncrementalCommandCanceledException((IncrementalCommand) cmd);
        }
        /*
        if (ex != null) {
        UncaughtExceptionHandler ueh = GWT.getUncaughtExceptionHandler();
        if (ueh != null) {
        ueh.onUncaughtException(ex);
        }
        }
         */
        if (ex != null) {
            ex.printStackTrace();
        }

        setExecuting(false);

        maybeStartExecutionTimer();
    }

    /**
     * This method will dispatch commands from the command queue. It will dispatch
     * commands until one of the following conditions is <code>true</code>:
     * <ul>
     * <li>It consumed its dispatching time slice
     * {@value #DEFAULT_TIME_SLICE_MILLIS}</li>
     * <li>It encounters a <code>null</code> in the command queue</li>
     * <li>All commands which were present at the start of the dispatching have
     * been removed from the command queue</li>
     * <li>The command that it was processing was canceled due to a false
     * cancellation -- in this case we exit without updating any state</li>
     * </ul>
     *
     * @param startTimeMillis the time when this method started
     */
    protected void doExecuteCommands(double startTimeMillis) {
        assert (!isExecutionTimerPending());

        boolean wasCanceled = false;
        try {
            setExecuting(true);

            iterator.setEnd(commands.size());

            //cancellationTimer.schedule(DEFAULT_CANCELLATION_TIMEOUT_MILLIS);
            timer.schedule(this.cancellationTask, DEFAULT_CANCELLATION_TIMEOUT_MILLIS);
            while (iterator.hasNext()) {
                Object element = iterator.next();

                boolean removeCommand = true;
                try {
                    if (element == null) {
                        // null forces a yield or pause in execution
                        return;
                    }

                    if (element instanceof Command) {
                        Command command = (Command) element;
                        command.execute();
                    } else if (element instanceof IncrementalCommand) {
                        IncrementalCommand incrementalCommand = (IncrementalCommand) element;
                        removeCommand = !incrementalCommand.execute();
                    }

                } finally {
                    wasCanceled = iterator.wasRemoved();
                    if (wasCanceled) {
                        /*
                         * The iterator may have already had its remove method called, if it
                         * has, then we need to exit without updating any state
                         */
                        return;
                    }

                    if (removeCommand) {
                        iterator.remove();
                    }
                }

                if (hasTimeSliceExpired(System.currentTimeMillis(), startTimeMillis)) {
                    // the time slice has expired
                    return;
                }
            }
        } finally {
            if (!wasCanceled) {
                //cancellationTimer.cancel();
                cancellationTask.cancel();

                setExecuting(false);

                maybeStartExecutionTimer();
            }
        }
    }

    /**
     * Starts the dispatch timer if there are commands to dispatch and we are not
     * waiting for a dispatch timer and we are not actively dispatching.
     */
    protected void maybeStartExecutionTimer() {
        if (!commands.isEmpty() && !isExecutionTimerPending() && !isExecuting()) {
            setExecutionTimerPending(true);
            //executionTimer.schedule(1);
            timer.schedule(executionTask, 1);
        }
    }

    /**
     * This method is for testing only.
     */
    List<Object> getPendingCommands() {
        return commands;
    }

    /**
     * This method is for testing only.
     */
    void setExecuting(boolean executing) {
        this.executing = executing;
    }

    /**
     * This method is for testing only.
     */
    void setLast(int last) {
        iterator.setLast(last);
    }

    /**
     * Returns <code>true</code> if this instance is currently dispatching
     * commands.
     *
     * @return <code>true</code> if this instance is currently dispatching
     *         commands
     */
    private boolean isExecuting() {
        return executing;
    }

    /**
     * Returns <code>true</code> if a the dispatch timer was scheduled but it
     * still has not fired.
     *
     * @return <code>true</code> if a the dispatch timer was scheduled but it
     *         still has not fired
     */
    private boolean isExecutionTimerPending() {
        return executionTimerPending;
    }

    private void setExecutionTimerPending(boolean pending) {
        executionTimerPending = pending;
    }
}